/* Copyright 2007 Sergejs Melderis (sergey.melderis@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

package com.sm.binder;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.AccessibleObject;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.sql.*;
import java.math.BigDecimal;

/**
 * 
 * @author Sergejs Melderis (sergey.melderis@gmail.com)
 */
public class ConstructorAnalizer<K> {

    private static interface Generator<T> {
        Random random = new Random(23451);
        T next();
    }

    private final static ConcurrentMap<Class<?>, ConstructorInfo<?>> cache =
            new ConcurrentHashMap<Class<?>, ConstructorInfo<?>>();

    private final static Map<Class<?>, Generator<?>> randomValues =
            new HashMap<Class<?>, Generator<?>>();

    static {
        randomValues.put(String.class, new Generator<String>() {
            public String next() { return String.valueOf(random.nextDouble()); }
        });
        randomValues.put(int.class, new Generator<Integer>() {
            public Integer next() { return random.nextInt(); }
        });
        randomValues.put(long.class, new Generator<Long>() {
            public Long next() { return random.nextLong(); }
        });
        randomValues.put(short.class, new Generator<Short>() {
            public Short next() { return (short) random.nextInt(); }
        });
        randomValues.put(byte.class, new Generator<Byte>() {
            public Byte next() { return (byte) random.nextInt(); }
        });
        randomValues.put(float.class, new Generator<Float>() {
            public Float next() { return random.nextFloat(); }
        });
        randomValues.put(double.class, new Generator<Double>() {
            public Double next() { return random.nextDouble(); }
        });
        randomValues.put(java.util.Date.class, new Generator<java.util.Date>() {
            public java.util.Date next() { return new java.util.Date(Math.abs(random.nextLong())); }
        });
        randomValues.put(java.sql.Date.class,  new Generator<java.sql.Date>() {
            public java.sql.Date next() { return new java.sql.Date(Math.abs(random.nextLong())); }
        });
        randomValues.put(Time.class, new Generator<Time>() {
            public Time next() { return new Time(Math.abs(random.nextLong())); }
        });
        randomValues.put(Timestamp.class, new Generator<Timestamp>() {
            public Timestamp next() { return new Timestamp(Math.abs(random.nextLong())); }
        });
        randomValues.put(BigDecimal.class, new Generator<BigDecimal>() {
            public BigDecimal next() { return new BigDecimal(random.nextDouble()); }
        });                                                                          
    }

    private final Class<K> clazz;

    /**
     * Creates an instance of ConstructorAnalizer
     * @param clazz
     */
    public ConstructorAnalizer(Class<K> clazz) {
        this.clazz = clazz;
    }

    @SuppressWarnings("unchecked")
    public ConstructorInfo<K> getParameterNames() {
        if (cache.containsKey(clazz)) {
            return (ConstructorInfo<K>) cache.get(clazz);
        }

        final Constructor<K>[] constructors = (Constructor<K>[]) clazz.getConstructors();

        List<ConstructorInfo<K>> paramNames =
                new ArrayList<ConstructorInfo<K>>();
        for (Constructor<K> c : constructors) {
            List<String> names = discoverParameterNames(c);
            if (names != null) {
                paramNames.add(new ConstructorInfo<K>(c, names));
            }
        }
        if (paramNames.size() == 0)
            return null;

        ConstructorInfo<K> longestParams = paramNames.get(0);
        for (ConstructorInfo c : paramNames) {
            if (c.parameterNames.size() > longestParams.parameterNames.size()) {
                longestParams = c;
            }
        }

        cache.put(clazz, longestParams);
        return longestParams;
    }

    /**
     * This is a hack of course. We create an object using some random values
     * which we pass to a constructor. Then we use instrospection to get
     * all the fields and then comparing those fields's values with our random
     * values. If we find a match then we assume the constructor's parameter name
     * is the name of matching field name, which is conventional in Java.
     * @param c Constructor
     * @return  List of parameter names or null if parameter names cannot be discovered.
     */
    private List<String> discoverParameterNames(Constructor<K> c) {
        Field [] fields = clazz.getDeclaredFields();
        try {
            AccessibleObject.setAccessible(fields, true);
        } catch (SecurityException e) {
            return null;
        }

        Class<?> []types = c.getParameterTypes();
        Object [] args = new Object[types.length];
        String []paramNames = new String[types.length];
        for (int i = 0; i < types.length; i++) {
            if (!randomValues.containsKey(types[i])) {
                //We support only limited set of types.
                return null;
            }
            args[i] = randomValues.get(types[i]).next();
        }

        try {
            Object obj = c.newInstance(args);
            for (Field f : fields) {
                Object value = f.get(obj);
                for (int i = 0; i < args.length; i++) {
                    if (args[i].equals(value)) {
                        paramNames[i] = f.getName();
                    }
                }
            }
        } catch (Exception e) {
            return null;
        }

        for (String paramName : paramNames) {
            if (paramName == null)
                return null;
        }
        return Arrays.asList(paramNames);
    }
}


class ConstructorInfo<K> {
    public final Constructor<K> constructor;

    public final List<String> parameterNames;

    public ConstructorInfo(Constructor<K> constructor, List<String> parameterNames) {
        this.constructor = constructor;
        this.parameterNames = parameterNames;
    }
}



